﻿using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace MetalSoda
{
    /// <summary>
    /// Generalised SSB host object representing one or more databases hosting an identical set of SSB services.
    /// </summary>
    [Serializable]
    public class ServiceHost : IDdlObject
    {        
        /// <summary>
        /// Service host name
        /// </summary>
        public readonly string Name;

        #region Private Fields
        readonly OneToManyAssociation<ServiceHost, Service> m_serviceAssociation;
        readonly OneToManyAssociation<ServiceHost, DatabaseInfo> m_databaseAssociations;

        readonly Dictionary<string, ServerInfo> m_ixServerInfoByAddress = new Dictionary<string, ServerInfo>(StringComparer.OrdinalIgnoreCase);
        readonly HashSet<EndPointObject> m_remoteEndPoints = new HashSet<EndPointObject>();
        readonly HashSet<RouteCollection> m_routes = new HashSet<RouteCollection>(); 
        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="MetalSoda.ServiceHost"/> class.
        /// </summary>
        /// <param name="name">Service host name</param>
        /// <param name="databases">Databases that belong to the new host instance</param>
        public ServiceHost(string name, params DatabaseInfo[] databases)
        {
            Name = name;

            m_serviceAssociation = new OneToManyAssociation<ServiceHost, Service>(this,
                added: args => args.Ref.AddHost(this),
                removed: args => args.Ref.RemoveHost(this));

            m_databaseAssociations = new OneToManyAssociation<ServiceHost, DatabaseInfo>(this,
                added: args => args.Ref.AddServiceHost(this),
                removed: args => args.Ref.RemoveServiceHost(this));
            Array.ForEach(databases, db => AddDatabase(db));            
        }

        /// <summary>
        /// Gets services hosted by this host.
        /// </summary>
        public IEnumerable<Service> Services { get { return m_serviceAssociation.AsEnumerable(); } }

        /// <summary>
        /// Gets queues hosted by this host.
        /// </summary>
        public IEnumerable<Queue> Queues { get { return Services.Select(s => s.Queue).Where(q => q != null).Distinct(); } }

        /// <summary>
        /// Gets contracts implemented by services hosted by this host.
        /// </summary>
        public IEnumerable<Contract> Contracts { get { return Services.SelectMany(s => s.Contracts).Distinct(); } }

        /// <summary>
        /// Gets message types that can be send/recieved by the services hosted by this host.
        /// </summary>
        public IEnumerable<MessageType> MessageTypes { get { return Services.SelectMany(s => s.MessageTypes).Distinct(); } }

        /// <summary>
        /// Gets SQL Server instances hosting the physical databases of this host
        /// </summary>
        public IEnumerable<ServerInfo> Servers { get { return m_databaseAssociations.Select(i => i.Server).Distinct(); } }

        /// <summary>
        /// Gets SQL Server databases that associated in this host
        /// </summary>
        public IEnumerable<DatabaseInfo> Databases { get { return m_databaseAssociations; } }

        /// <summary>
        /// Gets routes registered in this host.
        /// </summary>
        internal IEnumerable<Route> Routes
        { 
            get 
            {
                return
                    from collection in m_routes
                    from route in collection
                    select route;
            } 
        }

        /// <summary>
        /// Gets connection strings for databases of this host.
        /// </summary>
        public IEnumerable<string> ConnectionStrings
        {
            [DebuggerStepThrough]
            get
            {
                return
                    from databaseInfo in m_databaseAssociations
                    select databaseInfo.ConnectionString;
            }
        }

        /// <summary>
        /// Adds the specified database object to this host
        /// </summary>
        /// <param name="database">Database object</param>
        /// <returns>Self</returns>
        public ServiceHost AddDatabase(DatabaseInfo database)
        {

            if (m_databaseAssociations.Contains(database)) { return this; }
            m_databaseAssociations.Add(database);
            return this;
        }

        /// <summary>
        /// Removes the specified database from this host
        /// </summary>
        /// <param name="database"></param>
        /// <returns></returns>
        public ServiceHost RemoveDatabase(DatabaseInfo database)
        {
            m_databaseAssociations.Remove(database);
            return this;
        }                        

        /// <summary>
        /// Finds the brokers TCP port for the specified SQL Server instance.
        /// </summary>
        /// <param name="server">Server name</param>
        /// <returns>TCP port</returns>
        public string TcpPort(string server)
        {
            return m_ixServerInfoByAddress[server].SSBEndPointPort.ToString();
        }        

        /// <summary>
        /// Finds service endpoint for the specified service name.
        /// </summary>
        /// <param name="serviceName">Service name</param>
        /// <returns>Service endpoint</returns>
        public EndPointObject EndPoint(string serviceName)
        {
            var service = Service(serviceName);
            if (service == null) { throw new InvalidOperationException(); }
            return EndPoint(service);
        }

        /// <summary>
        /// Finds service endpoint for the specified service.
        /// </summary>
        /// <param name="service">Service</param>
        /// <returns>Service endpoint</returns>
        public EndPointObject EndPoint(Service service)
        {
            if (service == null) { throw new ArgumentNullException("service"); }
            if (false == m_serviceAssociation.Contains(service)) { throw new InvalidOperationException(); }
            return new EndPointObject(this, service);
        }
                         
        /// <summary>
        /// Finds service hosted by this host by the service name.
        /// </summary>
        /// <param name="serviceName">Service name</param>
        /// <returns>Service</returns>
        public Service Service(string serviceName)
        {
            return m_serviceAssociation.Where(s => Utils.AreSame(s.Name, serviceName)).SingleOrDefault();
        }

        /// <summary>
        /// Adds service to the services of this host
        /// </summary>
        /// <param name="service">Service</param>
        /// <returns>Self</returns>
        public ServiceHost AddService(Service service)
        {
            if (Service(service.Name) != null && Service(service.Name) != service)
            {
                var msg = string.Format("The {0} service already exists.", service.Name);
                throw new InvalidOperationException(msg);
            }
            m_serviceAssociation.Add(service);
            return this;
        }

        public ServiceHost RemoveService(Service service)
        {
            if (Object.ReferenceEquals(service, null)) { return this; }
            m_serviceAssociation.Remove(Service(service.Name));
            return this;
        }

        internal bool AddEndpoint(EndPointObject endpoint)
        {
            return m_remoteEndPoints.Add(endpoint);
        }

        public static void Deploy(params ServiceHost[] hosts)
        {
            foreach (var instance in hosts.SelectMany(h => h.Servers).Distinct())
            {
                instance.DeployInstanceConfigurations();                
            }
            foreach (var database in hosts.SelectMany(h => h.Databases).Distinct())
            {
                database.DeployDatabaseConfigurations();                
            }
            Array.ForEach(hosts, h => h.Deploy());
            //Array.ForEach(hosts, h => h.ConfigureServerRoutes());
            Array.ForEach(hosts, h => h.DeploySchema());
        }


        void Deploy()
        {
            foreach (var route in this.Routes)
            {
                route.Deploy();
            }
        }


        void ConfigureServerRoutes()
        {
            foreach (var connectionString in this.ConnectionStrings) 
            {
                var builder = new SqlConnectionStringBuilder(connectionString);
                var server = m_ixServerInfoByAddress[builder.DataSource];
                using (var connection = new SqlConnection(connectionString))
                {
                    connection.Open();
                    var command = connection.CreateCommand();
                    foreach (var route in m_routes)
                    {
                        //command.CommandText = new SQLScript.CreateRouteRuntimeTextTemplate(route).TransformText();
                        throw new NotImplementedException();
                        command.ExecuteNonQuery();
                    }
                }                
            }
        }

        private void DeploySchema()
        {
            foreach (var connectionString in this.ConnectionStrings)
            {
                using (var connection = new SqlConnection(connectionString))
                {
                    connection.Open();
                    var command = connection.CreateCommand();
                    foreach (var messageType in MessageTypes)
                    {
                        command.CommandText = new SQLScript.CreateMessageTypeRuntimeTextTemplate(messageType).TransformText();
                        command.ExecuteNonQuery();
                    }
                    foreach (var contract in Contracts)
                    {
                        command.CommandText = new SQLScript.CreateContractRuntimeTextTemplate(contract).TransformText();
                        command.ExecuteNonQuery();
                    }
                    foreach (var queues in Queues)
                    {
                        command.CommandText = new SQLScript.CreateQueueRuntimeTextTemplate(queues).TransformText();
                        command.ExecuteNonQuery();
                    }
                    foreach (var service in Services)
                    {
                        command.CommandText = new SQLScript.CreateServiceRuntimeTextTemplate(service).TransformText();
                        command.ExecuteNonQuery();
                    }
                }
            }
        }                       


        public IEnumerable<string> GenerateCreateBatches()
        {
            foreach (var batch in MessageTypes.SelectMany(mt=> mt.GenerateCreateBatches())) { yield return batch; }
            foreach (var batch in Contracts.SelectMany(c=> c.GenerateCreateBatches())) { yield return batch; }
            foreach (var batch in Queues.SelectMany(q => q.GenerateCreateBatches())) { yield return batch; }
            foreach (var batch in Services.SelectMany(s => s.GenerateCreateBatches())) { yield return batch; }
        }

        public IEnumerable<string> GenerateAlterBatches()
        {
            throw new NotImplementedException();
        }

        public IEnumerable<string> GenerateDeleteBatches()
        {
            throw new NotImplementedException();
        }



        //public Host RegisterDatabase(string database, string server = "localhost", bool autoCreateDatabase = false)
        //{
        //    server = server ?? "localhost";
        //    var dbInfo = new DatabaseInfo(database: database, server: server, autoCreate: autoCreateDatabase);
        //    var serverInfo = m_ixServerInfoByAddress.ContainsKey(dbInfo.ServerName) 
        //        ? m_ixServerInfoByAddress[dbInfo.ServerName]
        //        : (m_ixServerInfoByAddress[dbInfo.ServerName] = new ServerInfo(dbInfo.ServerName));
        //    serverInfo.AddDatabase(dbInfo);
        //    return this;
        //}


        /// <summary>
        /// 
        /// </summary>
        /// <param name="connectionString"></param>
        /// <param name="tcpPort">Tcp port (default = 2014)</param>
        /// <returns></returns>
        public ServiceHost RegisterConnectionString(string connectionString, int tcpPort = 20140)
        {
            if (connectionString == null) { throw new ArgumentNullException("connectionString"); }
            SqlConnectionStringBuilder builder = null;
            try
            {
                builder = new SqlConnectionStringBuilder(connectionString);
            }
            catch (FormatException) { }
            catch (KeyNotFoundException) { }
            catch (ArgumentException)
            {
                throw;
            }


            try 
            {
                
                using (var connection = new SqlConnection(connectionString)) 
                { 
                    connection.Open();
                    builder.DataSource = (string)new SqlCommand("SELECT @@SERVERNAME", connection).ExecuteScalar();
                    connectionString = builder.ConnectionString;
                }
            }
            catch (SqlException ex)
            {
                var message = new StringBuilder(@"Could not open connection to the database using the specified connection string");
                message.AppendFormat(@" (connection string: ""{0}""). ", connectionString);
                throw new InvalidOperationException(message.ToString(), ex);
            }
            catch (InvalidOperationException ex)
            {
                var message = new StringBuilder(@"Could not create connection object using the specified connection string");
                message.AppendFormat(@" (connection string: ""{0}""). Ensure that the provided string is in the correct format.", connectionString);
                throw new InvalidOperationException(message.ToString(), ex);
            }
                      

            ServerInfo serverInfo = null;
            if (false == m_ixServerInfoByAddress.TryGetValue(builder.DataSource, out serverInfo))
            {
                serverInfo = new ServerInfo(builder.DataSource);
                m_ixServerInfoByAddress.Add(builder.DataSource, serverInfo);
            }
            else if (false == serverInfo.SSBEndPointPort.Equals(tcpPort))
            {
                var message = new StringBuilder();
                message.AppendFormat(@"The specified tcp port value of {0} is in conflict with the earlier registered value of {1} (Server: {2})", tcpPort, serverInfo.SSBEndPointPort, builder.DataSource);
                throw new InvalidOperationException(message.ToString());
            }
            
            return this;
        }

        

        internal void RegisterRoutes(RouteCollection route)
        {
            if (!this.m_routes.Add(route)) { return; }
            //var duplicates = (from r in m_routes group r by r.Name into g where g.Count() > 1 select g.Key);
            //foreach (var dup in duplicates)
            //{
            //    var message = new StringBuilder("Duplicate route name.");
            //    message.AppendFormat(" The {0} route has been registered more than once on the {1} host.", dup, this.Name);
            //    message.AppendFormat(" Providing custom route names, ensure that all the registered names are unique.");
            //    throw new InvalidOperationException();
            //}
        }
    }
}
